using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.ComponentModel;
using System.Security.Permissions;
using System.Runtime.Serialization;
using System.Globalization;
using C4F.DevKit.WebServices.Properties;
using C4F.DevKit.WebServices.Datasets;

namespace C4F.DevKit.WebServices
{
    /// <summary>
    /// Exposes search functionality on YouTube.com
    /// </summary>
    [ToolboxItem(true)]
    public class YouTube : C4FWebServicesBase
    {
        #region Private members
        /// <summary>
        /// Base Url
        /// </summary>
        private static string baseUrl = Resources.YouTubeBaseUrl;
        /// <summary>
        /// Developer id required to use the api
        /// </summary>
        private string developerId = string.Empty;

        #endregion

        #region Accessors
        /// <summary>
        /// Gets/sets Developer id.
        /// </summary>
        [Category("YouTube")]
        [Description("Gets/sets Developer id.")]
        public string DeveloperId
        {
            get { return this.developerId == null ? string.Empty : this.developerId.Trim(); }
            set { this.developerId = value == null ? string.Empty : value.Trim(); }
        }

        #endregion

        #region Constructors
        /// <summary>
        /// Initializes a new object of YouTube class
        /// </summary>
        public YouTube()
        {
        }
        /// <summary>
        /// Initializes a new object of YouTube class with a developer id
        /// </summary>
        /// <param name="developerId">Developer id</param>
        public YouTube(string developerId)
        {
            this.developerId = developerId;
        }

        #endregion

        #region Public functions
        /// <summary>
        /// Lists all videos that have the specified tag
        /// </summary>
        /// <param name="tag">The tag to search for</param>
        /// <param name="numberOfPagesForResult">The "page" of results you want to retrieve</param>
        /// <param name="resultsPerPage">No. of results per page</param>
        /// <returns>A list of videos having the specified tag</returns>
        /// <exception cref="InvalidOperationException">The Developer Key is not set</exception>
        /// <exception cref="YouTubeException">YouTube Web service has raised an error</exception>
        public List<VideoData> GetVideoListByTag(string tag, int? numberOfPagesForResult, int? resultsPerPage)
        {
            if (String.IsNullOrEmpty(this.DeveloperId))
                throw new InvalidOperationException(Resources.ErrorDeveloperKeyNotSet);

            string requestUrl = this.GetQueryString(tag, numberOfPagesForResult,
                resultsPerPage);

            // Get YouTube responses
            string responseData = base.GetQueryResponse(requestUrl);

            // Parse the data and return results
            return YouTube.ParseResponseXml(responseData);
        }

        /// <summary>
        /// Lists all videos that have the specified tag
        /// </summary>
        /// <param name="tag">The tag to search for</param>
        /// <param name="numberOfPagesForResult">The "page" of results you want to retrieve</param>
        /// <param name="resultsPerPage">No. of results per page</param>
        /// <returns>Dataset containing the list of videos having the specified tag</returns>
        /// <exception cref="InvalidOperationException">The Developer Key is not set</exception>
        /// <exception cref="YouTubeException">YouTube Web service has raised an error</exception>
        public DS_YouTube GetVideoListByTag_DS(string tag, int? numberOfPagesForResult, int? resultsPerPage)
        {
            List<VideoData> videoList = GetVideoListByTag(tag, numberOfPagesForResult, resultsPerPage);
            DS_YouTube dataset = new DS_YouTube();
            foreach (VideoData video in videoList)
            {
                DS_YouTube.VideoDetailsRow videoDetailsRow = dataset.VideoDetails.AddVideoDetailsRow(video.VideoDetails.Author, video.VideoDetails.LengthInSeconds,
                    video.VideoDetails.ViewCount, video.VideoDetails.UploadTime, video.VideoDetails.CommentCount);

                DS_YouTube.RatingRow ratingRow = dataset.Rating.AddRatingRow(video.VideoDetails.Rating.NumberOfStars, video.VideoDetails.Rating.NumberOfVotes);

                dataset.VideoData.AddVideoDataRow(video.Id, video.Title, video.Description, video.Tags, video.Url, video.ThumbnailUrl,
                    videoDetailsRow, ratingRow);
            }
            return dataset;
        }

        #endregion

        #region Private functions

        /// <summary>
        /// Get query string based of specified parameters
        /// </summary>
        /// <param name="tag">The tag to search for</param>
        /// <param name="numberOfPagesForResult">The "page" of results you want to retrieve</param>
        /// <param name="resultsPerPage">No. of results per page</param>
        /// <returns>Request Url</returns>
        private string GetQueryString(string tag,
            int? numberOfPagesForResult, int? resultsPerPage)
        {
            StringBuilder queryBuilder = new StringBuilder(YouTube.baseUrl);

            // Add method name
            queryBuilder.Append("?method=youtube.videos.list_by_tag");

            // Add developer key
            queryBuilder.Append("&dev_id=");
            queryBuilder.Append(this.DeveloperId);

            // Add tag to search
            queryBuilder.Append("&tag=");
            queryBuilder.Append(tag);

            // Add numberOfPagesForResult
            if (numberOfPagesForResult.HasValue)
            {
                queryBuilder.Append("&page=");
                queryBuilder.Append(numberOfPagesForResult.Value);
            }

            // Add resultsPerPage
            if (resultsPerPage.HasValue)
            {
                queryBuilder.Append("&per_page=");
                queryBuilder.Append(resultsPerPage);
            }
            return queryBuilder.ToString();
        }

        /// <summary>
        /// Parses the response data and extracts the videos
        /// </summary>
        /// <param name="responseData">Response data</param>
        /// <returns>A list of videos matching the criteria</returns>
        private static List<VideoData> ParseResponseXml(string responseData)
        {
            XmlDocument xmlDocument = new XmlDocument();
            List<VideoData> videoDataList = null;

            xmlDocument.LoadXml(responseData);

            // Retrieve errors nodes
            XmlNode errorsNode = xmlDocument.DocumentElement.SelectSingleNode("error");
            if (errorsNode != null)
                throw new YouTubeException(errorsNode.SelectSingleNode("code").InnerText,
                                                errorsNode.SelectSingleNode("description").InnerText);


            // Retrieve result nodes
            XmlNodeList videoNodes =
                xmlDocument.DocumentElement.SelectNodes("video_list/video");

            if (videoNodes == null || videoNodes.Count == 0)
                return videoDataList;

            videoDataList = new List<VideoData>(videoNodes.Count);

            for (int i = 0; i < videoNodes.Count; i++)
            {
                XmlNode videoNode = videoNodes[i];
                VideoData videoData = new VideoData();
                videoData.VideoDetails = new VideoDetails();
                videoData.VideoDetails.Rating = new Rating();
                videoDataList.Add(videoData);

                // Populate VideoData object
                videoData.VideoDetails.Author =
                    videoNode.SelectSingleNode("author").InnerText;
                videoData.Id = videoNode.SelectSingleNode("id").InnerText;
                videoData.Title = videoNode.SelectSingleNode("title").InnerText;

                videoData.VideoDetails.LengthInSeconds = int.Parse(
                    videoNode.SelectSingleNode("length_seconds").InnerText, CultureInfo.InvariantCulture);

                videoData.VideoDetails.Rating.NumberOfStars = double.Parse(
                    videoNode.SelectSingleNode("rating_avg").InnerText, CultureInfo.InvariantCulture);

                videoData.VideoDetails.Rating.NumberOfVotes = int.Parse(videoNode.SelectSingleNode(
                    "rating_count").InnerText, CultureInfo.InvariantCulture);


                videoData.Description = videoNode.SelectSingleNode("description").InnerText;

                videoData.VideoDetails.ViewCount = int.Parse(videoNode.SelectSingleNode(
                    "view_count").InnerText, CultureInfo.InvariantCulture);

                videoData.VideoDetails.UploadTime = long.Parse(videoNode.SelectSingleNode(
                    "upload_time").InnerText, CultureInfo.InvariantCulture);

                videoData.VideoDetails.CommentCount = int.Parse(videoNode.SelectSingleNode(
                    "comment_count").InnerText, CultureInfo.InvariantCulture);

                videoData.Tags = videoNode.SelectSingleNode("tags").InnerText;

                videoData.Url = videoNode.SelectSingleNode("url").InnerText;

                videoData.ThumbnailUrl = videoNode.SelectSingleNode(
                    "thumbnail_url").InnerText;

            }
            return videoDataList;
        }
        #endregion
    }

    /// <summary>
    /// Represents a video on YouTube.com
    /// </summary>
    public class VideoData
    {
        #region Private Members
        /// <summary>
        /// Video id
        /// </summary>
        private string id;
        /// <summary>
        /// Title of the video
        /// </summary>
        private string title;
        /// <summary>
        /// The video description
        /// </summary>
        private string description;
        /// <summary>
        /// Tags associated with the video
        /// </summary>
        private string tags;
        /// <summary>
        /// Url of the video
        /// </summary>
        private string url;
        /// <summary>
        /// Url of the thumbnail
        /// </summary>
        private string thumbNailUrl;
        /// <summary>
        /// Video Details
        /// </summary>
        private VideoDetails videoDetails;
        #endregion

        #region Public Properties
        /// <summary>
        /// Video id
        /// </summary>
        public string Id
        {
            get { return this.id; }
            set { this.id = value; }
        }
        /// <summary>
        /// Title of the video
        /// </summary>
        public string Title
        {
            get { return this.title; }
            set { this.title = value; }
        }
        /// <summary>
        /// The video description
        /// </summary>
        public string Description
        {
            get { return this.description; }
            set { this.description = value; }
        }
        /// <summary>
        /// Tags associated with the video
        /// </summary>
        public string Tags
        {
            get { return this.tags; }
            set { this.tags = value; }
        }
        /// <summary>
        /// Url of the video
        /// </summary>
        public string Url
        {
            get { return this.url; }
            set { this.url = value; }
        }
        /// <summary>
        /// Url of the thumbnail
        /// </summary>
        public string ThumbnailUrl
        {
            get { return this.thumbNailUrl; }
            set { this.thumbNailUrl = value; }
        }
        /// <summary>
        /// Video Details
        /// </summary>
        public VideoDetails VideoDetails
        {
            get { return this.videoDetails; }
            set { this.videoDetails = value; }
        }
        #endregion
    }

    /// <summary>
    /// This class represents the video details
    /// </summary>
    public class VideoDetails
    {
        #region Private Members
        /// <summary>
        /// Author of the video
        /// </summary>
        private string author;
        /// <summary>
        /// Length of video
        /// </summary>
        private int lengthInSeconds;
        /// <summary>
        /// Rating that the video received
        /// </summary>
        private Rating rating;
        /// <summary>
        /// View counts
        /// </summary>
        private int viewCount;
        /// <summary>
        /// Upload time of the video
        /// </summary>
        private long uploadTime;
        /// <summary>
        /// No. of comments this video have
        /// </summary>
        private int commentCount;
        #endregion

        #region Public Properties
        /// <summary>
        /// Author of the video
        /// </summary>
        public string Author
        {
            get { return this.author; }
            set { this.author = value; }
        }
        /// <summary>
        /// Length of video
        /// </summary>
        public int LengthInSeconds
        {
            get { return this.lengthInSeconds; }
            set { this.lengthInSeconds = value; }
        }
        /// <summary>
        /// Rating that the video received
        /// </summary>
        public Rating Rating
        {
            get { return this.rating; }
            set { this.rating = value; }
        }
        /// <summary>
        /// View counts
        /// </summary>
        public int ViewCount
        {
            get { return this.viewCount; }
            set { this.viewCount = value; }
        }
        /// <summary>
        /// Upload time of the video
        /// </summary>
        public long UploadTime
        {
            get { return this.uploadTime; }
            set { this.uploadTime = value; }
        }
        /// <summary>
        /// No. of comments this video have
        /// </summary>
        public int CommentCount
        {
            get { return this.commentCount; }
            set { this.commentCount = value; }
        }
        #endregion
    }

    /// <summary>
    /// Exception Class to handle YouTube exceptions
    /// </summary>
    [global::System.Serializable]
    public class YouTubeException : Exception
    {
        /// <summary>
        /// Error code from error node
        /// </summary>
        private string errorCode;
        /// <summary>
        /// Required to throw the specific exception
        /// </summary>
        public string ErrorCode
        {
            get { return errorCode; }
            set { this.errorCode = value; }
        }
        /// <summary>
        /// Initializes a new object of YouTubeException class
        /// </summary>
        public YouTubeException() { }
        /// <summary>
        /// Initializes a new object of YouTubeException class
        /// </summary>
        /// <param name="errorCode">The code that represents the error</param>
        /// <param name="message">The message that describes the error</param>
        public YouTubeException(string errorCode, string message)
            : base(message)
        {
            this.ErrorCode = errorCode;
        }
        /// <summary>
        /// Initializes a new object of YouTubeException class
        /// </summary>
        /// <param name="message">The message that describes the error</param>
        public YouTubeException(string message) : base(message) { }
        /// <summary>
        /// Initializes a new object of YouTubeException class
        /// </summary>
        /// <param name="errorCode">The code that represents the error</param>
        /// <param name="message">The message that describes the error</param>
        /// <param name="inner">The exception that is the cause of the current exception</param>
        public YouTubeException(string errorCode, string message, Exception inner)
            : base(message, inner) 
        {
            this.ErrorCode = errorCode;
        }
        /// <summary>
        /// Initializes a new object of YouTubeException class
        /// </summary>
        /// <param name="message">The message that describes the error</param>
        /// <param name="inner">The exception that is the cause of the current exception</param>
        public YouTubeException(string message, Exception inner) : base(message, inner) { }
        /// <summary>
        /// Initializes a new object of YouTubeException class
        /// </summary>
        /// <param name="info">The System.Runtime.Serialization.SerializationInfo that holds the serialized
        /// object data about the exception being thrown.</param>
        /// <param name="context">The System.Runtime.Serialization.StreamingContext that contains contextual
        /// information about the source or destination.</param>
        protected YouTubeException(SerializationInfo info, StreamingContext context)
            : base(info, context) {
                this.ErrorCode = info.GetString("errorCode");
        }
        /// <summary>
        /// Sets the System.Runtime.Serialization.SerializationInfo
        /// with information about the exception.
        /// </summary>
        /// <param name="info">The System.Runtime.Serialization.SerializationInfo that holds the serialized
        /// object data about the exception being thrown.</param>
        /// <param name="context">The System.Runtime.Serialization.StreamingContext that contains contextual
        /// information about the source or destination.</param>
        [SecurityPermissionAttribute(SecurityAction.Demand,
            SerializationFormatter = true)]
        public override void GetObjectData(
            SerializationInfo info, StreamingContext context)
        {
            info.AddValue("errorCode", this.ErrorCode);
            base.GetObjectData(info, context);
        }
    }
}